<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSON 比较工具</title>
    <style>
        :root {
            --primary-color: #4a6fa5;
            --secondary-color: #6b8cae;
            --light-color: #f8f9fa;
            --dark-color: #343a40;
            --success-color: #28a745;
            --danger-color: #dc3545;
            --warning-color: #ffc107;
            --info-color: #17a2b8;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            color: var(--dark-color);
            background-color: #f5f5f5;
            margin: 0;
            padding: 0;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
        }
        
        header {
            background-color: var(--primary-color);
            color: white;
            padding: 20px 0;
            margin-bottom: 30px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        
        h1 {
            margin: 0;
            text-align: center;
            font-weight: 300;
        }
        
        .subtitle {
            text-align: center;
            margin-top: 5px;
            font-weight: 300;
            opacity: 0.9;
        }
        
        .comparison-container {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin-bottom: 20px;
        }
        
        @media (max-width: 768px) {
            .comparison-container {
                grid-template-columns: 1fr;
            }
        }
        
        .json-input {
            background-color: white;
            border-radius: 5px;
            padding: 15px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        
        .json-input h2 {
            margin-top: 0;
            color: var(--primary-color);
            font-size: 1.2rem;
        }
        
        textarea {
            width: 100%;
            height: 300px;
            font-family: 'Courier New', Courier, monospace;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            resize: vertical;
            font-size: 14px;
        }
        
        .controls {
            display: flex;
            justify-content: space-between;
            margin-bottom: 20px;
            flex-wrap: wrap;
            gap: 10px;
        }
        
        .options {
            background-color: white;
            padding: 15px;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            margin-bottom: 20px;
        }
        
        .options h2 {
            margin-top: 0;
            color: var(--primary-color);
            font-size: 1.2rem;
        }
        
        .option-group {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
        }
        
        .option-item {
            display: flex;
            align-items: center;
            gap: 5px;
        }
        
        button {
            background-color: var(--primary-color);
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        }
        
        button:hover {
            background-color: var(--secondary-color);
        }
        
        button.secondary {
            background-color: var(--light-color);
            color: var(--dark-color);
            border: 1px solid #ddd;
        }
        
        button.secondary:hover {
            background-color: #e9ecef;
        }
        
        .results {
            background-color: white;
            border-radius: 5px;
            padding: 15px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        
        .results h2 {
            margin-top: 0;
            color: var(--primary-color);
            font-size: 1.2rem;
        }
        
        .summary {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            margin-bottom: 20px;
        }
        
        .summary-item {
            background-color: #f8f9fa;
            padding: 10px 15px;
            border-radius: 4px;
            display: flex;
            align-items: center;
            gap: 5px;
        }
        
        .summary-item .count {
            font-weight: bold;
        }
        
        .summary-item.total .count {
            color: var(--primary-color);
        }
        
        .summary-item.significant .count {
            color: var(--danger-color);
        }
        
        .summary-item.added .count {
            color: var(--success-color);
        }
        
        .summary-item.removed .count {
            color: var(--danger-color);
        }
        
        .summary-item.changed .count {
            color: var(--warning-color);
        }
        
        .differences {
            max-height: 500px;
            overflow-y: auto;
            border: 1px solid #eee;
            border-radius: 4px;
        }
        
        .difference {
            padding: 10px 15px;
            border-bottom: 1px solid #eee;
            font-family: 'Courier New', Courier, monospace;
            font-size: 14px;
        }
        
        .difference:last-child {
            border-bottom: none;
        }
        
        .difference-header {
            display: flex;
            justify-content: space-between;
            margin-bottom: 5px;
        }
        
        .difference-path {
            font-weight: bold;
            color: var(--primary-color);
        }
        
        .difference-type {
            padding: 2px 8px;
            border-radius: 10px;
            font-size: 12px;
            text-transform: capitalize;
        }
        
        .difference-type.added {
            background-color: rgba(40, 167, 69, 0.1);
            color: var(--success-color);
        }
        
        .difference-type.removed {
            background-color: rgba(220, 53, 69, 0.1);
            color: var(--danger-color);
        }
        
        .difference-type.changed {
            background-color: rgba(255, 193, 7, 0.1);
            color: var(--warning-color);
        }
        
        .difference-type.type_change {
            background-color: rgba(23, 162, 184, 0.1);
            color: var(--info-color);
        }
        
        .difference-type.array_order {
            background-color: rgba(108, 117, 125, 0.1);
            color: #6c757d;
        }
        
        .difference-values {
            display: flex;
            gap: 10px;
            margin-top: 5px;
        }
        
        .difference-value {
            flex: 1;
            padding: 5px;
            border-radius: 3px;
            font-size: 13px;
        }
        
        .difference-value.old {
            background-color: rgba(220, 53, 69, 0.05);
            border-left: 3px solid var(--danger-color);
        }
        
        .difference-value.new {
            background-color: rgba(40, 167, 69, 0.05);
            border-left: 3px solid var(--success-color);
        }
        
        .ignored {
            opacity: 0.6;
        }
        
        .ignored-marker {
            font-size: 12px;
            color: #6c757d;
            margin-left: 5px;
        }
        
        .error {
            color: var(--danger-color);
            margin-top: 10px;
            padding: 10px;
            background-color: rgba(220, 53, 69, 0.1);
            border-radius: 4px;
            border-left: 3px solid var(--danger-color);
        }
        
        .tabs {
            display: flex;
            border-bottom: 1px solid #ddd;
            margin-bottom: 15px;
        }
        
        .tab {
            padding: 10px 20px;
            cursor: pointer;
            border-bottom: 3px solid transparent;
        }
        
        .tab.active {
            border-bottom-color: var(--primary-color);
            font-weight: bold;
        }
        
        .tab-content {
            display: none;
        }
        
        .tab-content.active {
            display: block;
        }
        
        .copy-button {
            background: none;
            border: none;
            color: var(--primary-color);
            cursor: pointer;
            padding: 0;
            font-size: 14px;
        }
        
        .copy-button:hover {
            text-decoration: underline;
        }
        
        footer {
            text-align: center;
            margin-top: 40px;
            padding: 20px 0;
            color: #6c757d;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <header>
        <div class="container">
            <h1>JSON 比较工具</h1>
            <div class="subtitle">快速比较两个 JSON 文件的结构和内容差异</div>
        </div>
    </header>
    
    <div class="container">
        <div class="controls">
            <div>
                <button id="compareBtn">比较 JSON</button>
                <button class="secondary" id="clearBtn">清空</button>
            </div>
            <div>
                <button class="secondary" id="exampleBtn">加载示例</button>
            </div>
        </div>
        
        <div class="options">
            <h2>比较选项</h2>
            <div class="option-group">
                <div class="option-item">
                    <input type="checkbox" id="ignoreWhitespace" checked>
                    <label for="ignoreWhitespace">忽略空格</label>
                </div>
                <div class="option-item">
                    <input type="checkbox" id="caseSensitive">
                    <label for="caseSensitive">区分大小写</label>
                </div>
                <div class="option-item">
                    <input type="checkbox" id="ignoreArrayOrder" checked>
                    <label for="ignoreArrayOrder">忽略数组顺序</label>
                </div>
                <div class="option-item">
                    <input type="checkbox" id="leftAsBase">
                    <label for="leftAsBase">以左边为基准比较</label>
                </div>
                <div class="option-item">
                    <label for="numericPrecision">数值精度:</label>
                    <input type="number" id="numericPrecision" value="0.000001" step="0.000001" min="0" style="width: 80px;">
                </div>
            </div>
            <div class="option-group" style="margin-top: 10px;">
                <div class="option-item">
                    <label for="ignoredKeys">忽略的键名:</label>
                    <input type="text" id="ignoredKeys" placeholder="用逗号分隔" style="width: 200px;">
                </div>
            </div>
        </div>
        
        <div class="comparison-container">
            <div class="json-input">
                <h2>JSON 1</h2>
                <textarea id="json1" placeholder="在此粘贴第一个 JSON..."></textarea>
            </div>
            <div class="json-input">
                <h2>JSON 2</h2>
                <textarea id="json2" placeholder="在此粘贴第二个 JSON..."></textarea>
            </div>
        </div>
        
        <div class="results" id="results" style="display: none;">
            <h2>比较结果</h2>
            <div id="errorContainer" style="display: none;"></div>
            
            <div class="tabs">
                <div class="tab active" data-tab="summary">摘要</div>
                <div class="tab" data-tab="differences">差异详情</div>
                <div class="tab" data-tab="raw">原始数据</div>
            </div>
            
            <div class="tab-content active" id="summary-tab">
                <div class="summary" id="summaryContainer"></div>
                <div id="noDifferences" style="display: none; text-align: center; padding: 20px; color: var(--success-color);">
                    <h3>没有发现差异！</h3>
                    <p>两个 JSON 文件内容完全相同。</p>
                </div>
            </div>
            
            <div class="tab-content" id="differences-tab">
                <div class="differences" id="differencesContainer"></div>
            </div>
            
            <div class="tab-content" id="raw-tab">
                <button class="copy-button" id="copyRawBtn">复制到剪贴板</button>
                <pre id="rawOutput" style="background: #f8f9fa; padding: 10px; border-radius: 4px; max-height: 400px; overflow: auto;"></pre>
            </div>
        </div>
    </div>
    
    <footer>
        <div class="container">
            <p>JSON 比较工具 &copy; 2025 | 使用纯 JavaScript 实现</p>
        </div>
    </footer>
    
    <script>
        // JSON 比较器实现
        class JSONComparator {
            constructor(options = {}) {
                this.options = {
                    ignoreWhitespace: true,
                    caseSensitive: false,
                    ignoreArrayOrder: false,
                    leftAsBase: false,
                    numericPrecision: 0.000001,
                    ignoredKeys: [],
                    ...options
                };
            }

            compare(objA, objB) {
                this.differences = [];
                
                if (this.options.leftAsBase) {
                    this._compareWithLeftAsBase(objA, objB, '$');
                } else {
                    this._compareValues(objA, objB, '$');
                }
                
                return {
                    differences: this.differences,
                    summary: this._generateSummary()
                };
            }

            _compareWithLeftAsBase(objA, objB, path) {
                // 只比较左边对象中存在的字段
                const keysA = new Set(Object.keys(objA));
                
                keysA.forEach(key => {
                    if (!this._isKeyIgnored(key)) {
                        if (objB && objB.hasOwnProperty(key)) {
                            this._compareValues(objA[key], objB[key], `${path}.${key}`);
                        } else {
                            this._recordDifference('removed', `${path}.${key}`, objA[key], undefined);
                        }
                    }
                });
            }

            _generateSummary() {
                const summary = {
                    totalDifferences: this.differences.length,
                    significantDifferences: this.differences.filter(d => !d.ignored).length,
                    added: 0,
                    removed: 0,
                    changed: 0,
                    typeChanges: 0,
                    arrayOrderChanges: 0
                };

                this.differences.forEach(diff => {
                    if (diff.type === 'added') summary.added++;
                    else if (diff.type === 'removed') summary.removed++;
                    else if (diff.type === 'type_change') summary.typeChanges++;
                    else if (diff.type === 'array_order') summary.arrayOrderChanges++;
                    else summary.changed++;
                });

                return summary;
            }

            _compareValues(valueA, valueB, path) {
                if (valueA === undefined && valueB === undefined) return;
                
                if (this._getType(valueA) !== this._getType(valueB)) {
                    this._recordDifference('type_change', path, valueA, valueB);
                    return;
                }

                const type = this._getType(valueA);
                switch (type) {
                    case 'object':
                        this._compareObjects(valueA, valueB, path);
                        break;
                    case 'array':
                        this._compareArrays(valueA, valueB, path);
                        break;
                    case 'string':
                        this._compareStrings(valueA, valueB, path);
                        break;
                    case 'number':
                        this._compareNumbers(valueA, valueB, path);
                        break;
                    case 'boolean':
                    case 'null':
                        if (valueA !== valueB) {
                            this._recordDifference('changed', path, valueA, valueB);
                        }
                        break;
                    default:
                        if (valueA !== valueB) {
                            this._recordDifference('changed', path, valueA, valueB);
                        }
                }
            }

            _compareObjects(objA, objB, path) {
                const keysA = new Set(Object.keys(objA));
                const keysB = new Set(Object.keys(objB));

                keysA.forEach(key => {
                    if (!keysB.has(key) && !this._isKeyIgnored(key)) {
                        this._recordDifference('removed', `${path}.${key}`, objA[key], undefined);
                    }
                });

                if (!this.options.leftAsBase) {
                    keysB.forEach(key => {
                        if (!keysA.has(key) && !this._isKeyIgnored(key)) {
                            this._recordDifference('added', `${path}.${key}`, undefined, objB[key]);
                        }
                    });
                }

                keysA.forEach(key => {
                    if (keysB.has(key) && !this._isKeyIgnored(key)) {
                        this._compareValues(objA[key], objB[key], `${path}.${key}`);
                    }
                });
            }

            _compareArrays(arrA, arrB, path) {
                if (arrA.length !== arrB.length) {
                    this._recordDifference('array_length', path, arrA.length, arrB.length);
                    return;
                }

                if (this.options.ignoreArrayOrder) {
                    this._compareArraysIgnoreOrder(arrA, arrB, path);
                } else {
                    this._compareArraysWithOrder(arrA, arrB, path);
                }
            }

            _compareArraysWithOrder(arrA, arrB, path) {
                for (let i = 0; i < arrA.length; i++) {
                    this._compareValues(arrA[i], arrB[i], `${path}[${i}]`);
                }
            }

            _compareArraysIgnoreOrder(arrA, arrB, path) {
                const matchedIndices = new Set();
                
                outer: for (let i = 0; i < arrA.length; i++) {
                    for (let j = 0; j < arrB.length; j++) {
                        if (!matchedIndices.has(j)) {
                            const tempDifferences = [];
                            const originalDifferences = this.differences;
                            this.differences = tempDifferences;
                            
                            this._compareValues(arrA[i], arrB[j], `${path}[${i}]`);
                            
                            this.differences = originalDifferences;
                            
                            if (tempDifferences.length === 0) {
                                matchedIndices.add(j);
                                continue outer;
                            }
                        }
                    }
                    
                    this._recordDifference('array_element_missing', `${path}[${i}]`, arrA[i], undefined);
                }
                
                if (!this._arraysHaveSameElements(arrA, arrB)) {
                    this._recordDifference('array_order', path, arrA, arrB, true);
                }
            }

            _compareStrings(strA, strB, path) {
                let a = strA;
                let b = strB;
                
                if (!this.options.caseSensitive) {
                    a = a.toLowerCase();
                    b = b.toLowerCase();
                }
                
                if (this.options.ignoreWhitespace) {
                    a = a.trim();
                    b = b.trim();
                }
                
                if (a !== b) {
                    this._recordDifference('changed', path, strA, strB);
                }
            }

            _compareNumbers(numA, numB, path) {
                if (Math.abs(numA - numB) > this.options.numericPrecision) {
                    this._recordDifference('changed', path, numA, numB);
                } else if (numA !== numB) {
                    this._recordDifference('numeric_precision', path, numA, numB, true);
                }
            }

            _recordDifference(type, path, valueA, valueB, ignored = false) {
                this.differences.push({
                    type,
                    path,
                    valueA,
                    valueB,
                    ignored,
                    timestamp: new Date().toISOString()
                });
            }

            _isKeyIgnored(key) {
                return this.options.ignoredKeys.includes(key);
            }

            _getType(value) {
                if (value === null) return 'null';
                if (Array.isArray(value)) return 'array';
                return typeof value;
            }

            _arraysHaveSameElements(arrA, arrB) {
                if (arrA.length !== arrB.length) return false;
                
                const elementCounts = new Map();
                
                for (const elem of arrA) {
                    const key = JSON.stringify(elem);
                    elementCounts.set(key, (elementCounts.get(key) || 0) + 1);
                }
                
                for (const elem of arrB) {
                    const key = JSON.stringify(elem);
                    if (!elementCounts.has(key)) return false;
                    
                    const count = elementCounts.get(key);
                    if (count === 1) {
                        elementCounts.delete(key);
                    } else {
                        elementCounts.set(key, count - 1);
                    }
                }
                
                return elementCounts.size === 0;
            }
        }

        // DOM 操作和事件处理
        document.addEventListener('DOMContentLoaded', function() {
            const compareBtn = document.getElementById('compareBtn');
            const clearBtn = document.getElementById('clearBtn');
            const exampleBtn = document.getElementById('exampleBtn');
            const json1Textarea = document.getElementById('json1');
            const json2Textarea = document.getElementById('json2');
            const resultsDiv = document.getElementById('results');
            const errorContainer = document.getElementById('errorContainer');
            const summaryContainer = document.getElementById('summaryContainer');
            const differencesContainer = document.getElementById('differencesContainer');
            const noDifferencesDiv = document.getElementById('noDifferences');
            const rawOutput = document.getElementById('rawOutput');
            const copyRawBtn = document.getElementById('copyRawBtn');
            const tabs = document.querySelectorAll('.tab');
            const tabContents = document.querySelectorAll('.tab-content');
            
            // 示例 JSON
            const exampleJson1 = {
                name: "John Doe",
                age: 30,
                active: true,
                hobbies: ["reading", "swimming"],
                address: {
                    street: "123 Main St",
                    city: "New York"
                },
                scores: [95, 88, 92]
            };
            
            const exampleJson2 = {
                name: "john doe",
                age: 30.00001,
                active: true,
                hobbies: ["swimming", "reading"],
                address: {
                    street: "123 Main St",
                    city: "New York",
                    zip: "10001"
                },
                scores: [95, 88, 92],
                newField: "added"
            };
            
            // 比较按钮点击事件
            compareBtn.addEventListener('click', function() {
                try {
                    errorContainer.style.display = 'none';
                    errorContainer.innerHTML = '';
                    
                    const json1 = json1Textarea.value.trim();
                    const json2 = json2Textarea.value.trim();
                    
                    if (!json1 || !json2) {
                        throw new Error('请在两边的文本框中输入 JSON 内容');
                    }
                    
                    const obj1 = JSON.parse(json1);
                    const obj2 = JSON.parse(json2);
                    
                    const options = {
                        ignoreWhitespace: document.getElementById('ignoreWhitespace').checked,
                        caseSensitive: document.getElementById('caseSensitive').checked,
                        ignoreArrayOrder: document.getElementById('ignoreArrayOrder').checked,
                        leftAsBase: document.getElementById('leftAsBase').checked,
                        numericPrecision: parseFloat(document.getElementById('numericPrecision').value),
                        ignoredKeys: document.getElementById('ignoredKeys').value
                            .split(',')
                            .map(k => k.trim())
                            .filter(k => k)
                    };
                    
                    const comparator = new JSONComparator(options);
                    const result = comparator.compare(obj1, obj2);
                    
                    displayResults(result);
                    
                } catch (error) {
                    errorContainer.style.display = 'block';
                    errorContainer.innerHTML = `<div class="error">${error.message}</div>`;
                    resultsDiv.style.display = 'none';
                }
            });
            
            // 清空按钮点击事件
            clearBtn.addEventListener('click', function() {
                json1Textarea.value = '';
                json2Textarea.value = '';
                resultsDiv.style.display = 'none';
                errorContainer.style.display = 'none';
            });
            
            // 示例按钮点击事件
            exampleBtn.addEventListener('click', function() {
                json1Textarea.value = JSON.stringify(exampleJson1, null, 2);
                json2Textarea.value = JSON.stringify(exampleJson2, null, 2);
                resultsDiv.style.display = 'none';
                errorContainer.style.display = 'none';
            });
            
            // 复制原始数据按钮点击事件
            copyRawBtn.addEventListener('click', function() {
                const text = rawOutput.textContent;
                navigator.clipboard.writeText(text).then(() => {
                    const originalText = copyRawBtn.textContent;
                    copyRawBtn.textContent = '已复制！';
                    setTimeout(() => {
                        copyRawBtn.textContent = originalText;
                    }, 2000);
                });
            });
            
            // 标签页切换
            tabs.forEach(tab => {
                tab.addEventListener('click', function() {
                    const tabId = this.getAttribute('data-tab');
                    
                    tabs.forEach(t => t.classList.remove('active'));
                    tabContents.forEach(c => c.classList.remove('active'));
                    
                    this.classList.add('active');
                    document.getElementById(`${tabId}-tab`).classList.add('active');
                });
            });
            
            // 显示比较结果
            function displayResults(result) {
                resultsDiv.style.display = 'block';
                
                // 显示原始数据
                rawOutput.textContent = JSON.stringify(result, null, 2);
                
                // 显示摘要
                displaySummary(result.summary);
                
                // 显示差异详情
                displayDifferences(result.differences);
                
                // 如果没有差异
                if (result.differences.length === 0) {
                    noDifferencesDiv.style.display = 'block';
                    summaryContainer.style.display = 'none';
                } else {
                    noDifferencesDiv.style.display = 'none';
                    summaryContainer.style.display = 'flex';
                }
            }
            
            // 显示摘要
            function displaySummary(summary) {
                summaryContainer.innerHTML = `
                    <div class="summary-item total">
                        <span>总差异:</span>
                        <span class="count">${summary.totalDifferences}</span>
                    </div>
                    <div class="summary-item significant">
                        <span>重要差异:</span>
                        <span class="count">${summary.significantDifferences}</span>
                    </div>
                    <div class="summary-item added">
                        <span>新增:</span>
                        <span class="count">${summary.added}</span>
                    </div>
                    <div class="summary-item removed">
                        <span>删除:</span>
                        <span class="count">${summary.removed}</span>
                    </div>
                    <div class="summary-item changed">
                        <span>修改:</span>
                        <span class="count">${summary.changed}</span>
                    </div>
                    <div class="summary-item">
                        <span>类型变化:</span>
                        <span class="count">${summary.typeChanges}</span>
                    </div>
                    <div class="summary-item">
                        <span>数组顺序:</span>
                        <span class="count">${summary.arrayOrderChanges}</span>
                    </div>
                `;
            }
            
            // 显示差异详情
            function displayDifferences(differences) {
                differencesContainer.innerHTML = '';
                
                if (differences.length === 0) {
                    differencesContainer.innerHTML = '<div class="difference">没有发现差异</div>';
                    return;
                }
                
                differences.forEach(diff => {
                    const diffElement = document.createElement('div');
                    diffElement.className = `difference ${diff.ignored ? 'ignored' : ''}`;
                    
                    const typeClass = diff.type.includes('_') ? 
                        diff.type.replace('_', '-') : diff.type;
                    
                    diffElement.innerHTML = `
                        <div class="difference-header">
                            <span class="difference-path">${diff.path}</span>
                            <span class="difference-type ${typeClass}">${diff.type.replace('_', ' ')}</span>
                        </div>
                        <div class="difference-values">
                            <div class="difference-value old">
                                <strong>原值:</strong> ${formatValue(diff.valueA)}
                            </div>
                            <div class="difference-value new">
                                <strong>新值:</strong> ${formatValue(diff.valueB)}
                            </div>
                        </div>
                        ${diff.ignored ? '<div class="ignored-marker">(已忽略)</div>' : ''}
                    `;
                    
                    differencesContainer.appendChild(diffElement);
                });
            }
            
            // 格式化值显示
            function formatValue(value) {
                if (value === undefined) return 'undefined';
                if (value === null) return 'null';
                if (typeof value === 'object') {
                    return JSON.stringify(value);
                }
                return value;
            }
        });
    </script>
</body>
</html>